/*
 * Copyright 2012-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.loader;

import lol.clann.minecraft.springboot.bukkit.plugin.AdapterContext;
import lol.clann.minecraft.springboot.bukkit.plugin.bukkit.AdapterPlugin;
import lol.clann.minecraft.springboot.bukkit.utils.BukkitUtils;
import lol.clann.minecraft.springboot.remap.ClassLoaderContext;
import lol.clann.minecraft.springboot.remap.RemapUtils;
import lol.clann.minecraft.springboot.remap.context.RemapContext;
import org.bukkit.plugin.PluginLoader;
import org.springframework.boot.loader.jar.Handler;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.CodeSigner;
import java.security.CodeSource;
import java.security.PrivilegedExceptionAction;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Map;
import java.util.Objects;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * {@link ClassLoader} used by the {@link Launcher}.
 *
 * @author Phillip Webb
 * @author Dave Syer
 * @author Andy Wilkinson
 */
public class LaunchedURLClassLoader extends URLClassLoader {
    private static final Pattern cbPattern = Pattern.compile("org\\.bukkit\\.craftbukkit\\.(v\\d_\\d+_R\\d)\\.[\\w.]+");
    private static final Pattern nmsPattern = Pattern.compile("net\\.minecraft\\.server\\.(v\\d_\\d+_R\\d)\\.[\\w.]+");
    private static final String targetVersion = BukkitUtils.getPackageVersion();
    private static PluginLoader PLUGIN_LOADER;
    private static final Collection<URLClassLoader> PLUGIN_CLASS_LOADERS;

    static {
        ClassLoader.registerAsParallelCapable();
        try {
            PLUGIN_LOADER = AdapterPlugin.plugin.getPluginLoader();
            if (PLUGIN_LOADER.getClass().getName().equals("us.Myles.PWP.TransparentListeners.PerWorldPluginLoader")) {
//                这是不同世界加载不同插件 的插件
                Field internal_loader = PLUGIN_LOADER.getClass().getDeclaredField("internal_loader");
                internal_loader.setAccessible(true);
                PLUGIN_LOADER = (PluginLoader) internal_loader.get(PLUGIN_LOADER);
            }
            Field fieldLoaders = PLUGIN_LOADER.getClass().getDeclaredField("loaders");
            fieldLoaders.setAccessible(true);
            Collection<URLClassLoader> pluginClassLoaders;
            if (Map.class.isAssignableFrom(fieldLoaders.getType())) {
//                低版本是map
                pluginClassLoaders = ((Map) fieldLoaders.get(PLUGIN_LOADER)).values();
            } else {
//                高版本是list
                pluginClassLoaders = (Collection<URLClassLoader>) fieldLoaders.get(PLUGIN_LOADER);
            }
            PLUGIN_CLASS_LOADERS = pluginClassLoaders;
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    public String map(String typeName) {
        String str = typeName;
        Matcher m = cbPattern.matcher(str);
        if (m.find()) {
            String srcVersion = m.group(1);
            if (!Objects.equals(srcVersion, targetVersion)) {
                str = str.replace(srcVersion, targetVersion);
            }
        } else {
            m = nmsPattern.matcher(typeName);
            if (m.find()) {
                String srcVersion = m.group(1);
                if (!Objects.equals(srcVersion, targetVersion)) {
                    str = str.replace(srcVersion, targetVersion);
                }
            }
        }
        return str;
    }

    public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
        RemapUtils.sbpClassLoader = this;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String str = name;
        Matcher m = cbPattern.matcher(str);
        if (m.find()) {
            String srcVersion = m.group(1);
            str = str.replace(srcVersion, targetVersion);
            return super.findClass(str);
        } else {
            m = nmsPattern.matcher(name);
            if (m.find()) {
                String srcVersion = m.group(1);
                str = str.replace(srcVersion, targetVersion);
                str = RemapUtils.map(str.replace('.', '/')).replace('/', '.');
                return super.findClass(str);
            }
        }
        if (!RemapContext.shouldRemap(name)) {
            return super.findClass(name);
        }
        ClassLoaderContext.put(this);
        Class<?> result = null;
        try {
            // Load the resource to the name
            String path = name.replace('.', '/').concat(".class");
            URL url = this.findResource(path);
            if (url != null) {
                InputStream stream = url.openStream();
                if (stream != null) {
//                  remap
                    ByteArrayOutputStream out = new ByteArrayOutputStream();
                    byte[] bs = new byte[1024];
                    int n = -1;
                    while ((n = stream.read(bs)) != -1) {
                        out.write(bs, 0, n);
                    }
                    stream.close();
                    byte[] bytecode = RemapUtils.remapFindClass(name, out.toByteArray());
                    // Define (create) the class using the modified byte code
                    // The top-child class loader is used for this to prevent access violations
                    // Set the codesource to the jar, not within the jar, for compatibility with
                    // plugins that do new File(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()))
                    // instead of using getResourceAsStream - see https://github.com/MinecraftPortCentral/Cauldron-Plus/issues/75
                    JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); // parses only
                    URL jarURL = jarURLConnection.getJarFileURL();
                    CodeSource codeSource = new CodeSource(jarURL, new CodeSigner[0]);
                    result = this.defineClass(name, bytecode, 0, bytecode.length, codeSource);
                    if (result != null) {
                        // Resolve it - sets the class loader of the class
                        this.resolveClass(result);
                    }
                }
            }
        } catch (Throwable t) {
            throw new ClassNotFoundException("Failed to remap class " + name, t);
        } finally {
            ClassLoaderContext.pop();
        }
        return result;
    }

    @Override
    public URL findResource(String name) {
        Handler.setUseFastConnectionExceptions(true);
        try {
            URL url = super.findResource(name);
            if (url == null) {
                if ((name.startsWith("org/bukkit/") || name.startsWith("net/minecraft/")) && isErrorPaper()) {
//                    1.15.2的paper端有bug,他的PluginClassLoader不能加载父类资源
                    URLClassLoader parent = (URLClassLoader) getParent().getParent();
                    return parent.findResource(name);
                } else if (!name.startsWith("org/springframework/")) {
//                非spring的类,在自己的classloader里没找到,去其他插件的classloader里找
                    for (URLClassLoader pluginClassLoader : PLUGIN_CLASS_LOADERS) {
                        if (pluginClassLoader == this.getParent()) {
//                        忽略自己
                            continue;
                        }
                        url = pluginClassLoader.findResource(name);
                        if (url != null) {
                            break;
                        }
                    }
                }
            }
            return url;
        } finally {
            Handler.setUseFastConnectionExceptions(false);
        }
    }

    @Override
    public Enumeration<URL> findResources(String name) throws IOException {
        Handler.setUseFastConnectionExceptions(true);
        try {
            return new UseFastConnectionExceptionsEnumeration(super.findResources(name));
        } finally {
            Handler.setUseFastConnectionExceptions(false);
        }
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        Handler.setUseFastConnectionExceptions(true);
        try {
            try {
                definePackageIfNecessary(name);
            } catch (IllegalArgumentException ex) {
                // Tolerate race condition due to being parallel capable
                if (getPackage(name) == null) {
                    // This should never happen as the IllegalArgumentException indicates
                    // that the package has already been defined and, therefore,
                    // getPackage(name) should not return null.
                    throw new AssertionError("Package " + name + " has already been "
                            + "defined but it could not be found");
                }
            }
            if ((name.startsWith("org.bukkit.") || name.startsWith("net.minecraft.")) && isErrorPaper()) {
//                1.15.2的paper端有bug
                return getParent().getParent().loadClass(name);
            }
            return super.loadClass(name, resolve);
        } finally {
            Handler.setUseFastConnectionExceptions(false);
        }
    }

    private boolean isErrorPaper() {
        return AdapterContext.isBukkit()
                && AdapterContext.isPaper()
                && BukkitUtils.getBukkitVersionNumber() == 11502;
    }

    private void definePackageIfNecessary(String className) {
        int lastDot = className.lastIndexOf('.');
        if (lastDot >= 0) {
            String packageName = className.substring(0, lastDot);
            if (getPackage(packageName) == null) {
                try {
                    definePackage(className, packageName);
                } catch (IllegalArgumentException ex) {
                    // Tolerate race condition due to being parallel capable
                    if (getPackage(packageName) == null) {
                        // This should never happen as the IllegalArgumentException
                        // indicates that the package has already been defined and,
                        // therefore, getPackage(name) should not have returned null.
                        throw new AssertionError(
                                "Package " + packageName + " has already been defined "
                                        + "but it could not be found");
                    }
                }
            }
        }
    }

    private void definePackage(String className, String packageName) {
        try {
            AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                String packageEntryName = packageName.replace('.', '/') + "/";
                String classEntryName = className.replace('.', '/') + ".class";
                for (URL url : getURLs()) {
                    try {
                        URLConnection connection = url.openConnection();
                        if (connection instanceof JarURLConnection) {
                            JarFile jarFile = ((JarURLConnection) connection)
                                    .getJarFile();
                            if (jarFile.getEntry(classEntryName) != null
                                    && jarFile.getEntry(packageEntryName) != null
                                    && jarFile.getManifest() != null) {
                                definePackage(packageName, jarFile.getManifest(), url);
                                return null;
                            }
                        }
                    } catch (IOException ex) {
                        // Ignore
                    }
                }
                return null;
            }, AccessController.getContext());
        } catch (java.security.PrivilegedActionException ex) {
            // Ignore
        }
    }

    /**
     * Clear URL caches.
     */
    public void clearCache() {
        for (URL url : getURLs()) {
            try {
                URLConnection connection = url.openConnection();
                if (connection instanceof JarURLConnection) {
                    clearCache(connection);
                }
            } catch (IOException ex) {
                // Ignore
            }
        }

    }

    private void clearCache(URLConnection connection) throws IOException {
        Object jarFile = ((JarURLConnection) connection).getJarFile();
        if (jarFile instanceof org.springframework.boot.loader.jar.JarFile) {
            ((org.springframework.boot.loader.jar.JarFile) jarFile).clearCache();
        }
    }

    private static class UseFastConnectionExceptionsEnumeration
            implements Enumeration<URL> {

        private final Enumeration<URL> delegate;

        UseFastConnectionExceptionsEnumeration(Enumeration<URL> delegate) {
            this.delegate = delegate;
        }

        @Override
        public boolean hasMoreElements() {
            Handler.setUseFastConnectionExceptions(true);
            try {
                return this.delegate.hasMoreElements();
            } finally {
                Handler.setUseFastConnectionExceptions(false);
            }

        }

        @Override
        public URL nextElement() {
            Handler.setUseFastConnectionExceptions(true);
            try {
                return this.delegate.nextElement();
            } finally {
                Handler.setUseFastConnectionExceptions(false);
            }
        }

    }

}
